package com.baomidou.kisso.common.bcprov.asn1;

import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

import com.baomidou.kisso.common.bcprov.util.io.Streams;

/**
 * a general purpose ASN.1 decoder - note: this class differs from the
 * others in that it returns null after it has read the last object in
 * the stream. If an ASN.1 NULL is encountered a DER/BER Null object is
 * returned.
 */
public class ASN1InputStream extends FilterInputStream implements BERTags {

	private final int limit;

	private final boolean lazyEvaluate;

	private final byte[][] tmpBuffers;


	public ASN1InputStream( InputStream is ) {
		this(is, StreamUtil.findLimit(is));
	}


	/**
	 * Create an ASN1InputStream based on the input byte array. The length of DER objects in
	 * the stream is automatically limited to the length of the input array.
	 * 
	 * @param input array containing ASN.1 encoded data.
	 */
	public ASN1InputStream( byte[] input ) {
		this(new ByteArrayInputStream(input), input.length);
	}


	/**
	 * Create an ASN1InputStream based on the input byte array. The length of DER objects in
	 * the stream is automatically limited to the length of the input array.
	 *
	 * @param input array containing ASN.1 encoded data.
	 * @param lazyEvaluate true if parsing inside constructed objects can be delayed.
	 */
	public ASN1InputStream( byte[] input, boolean lazyEvaluate ) {
		this(new ByteArrayInputStream(input), input.length, lazyEvaluate);
	}


	/**
	 * Create an ASN1InputStream where no DER object will be longer than limit.
	 * 
	 * @param input stream containing ASN.1 encoded data.
	 * @param limit maximum size of a DER encoded object.
	 */
	public ASN1InputStream( InputStream input, int limit ) {
		this(input, limit, false);
	}


	/**
	 * Create an ASN1InputStream where no DER object will be longer than limit, and constructed
	 * objects such as sequences will be parsed lazily.
	 *
	 * @param input stream containing ASN.1 encoded data.
	 * @param lazyEvaluate true if parsing inside constructed objects can be delayed.
	 */
	public ASN1InputStream( InputStream input, boolean lazyEvaluate ) {
		this(input, StreamUtil.findLimit(input), lazyEvaluate);
	}


	/**
	 * Create an ASN1InputStream where no DER object will be longer than limit, and constructed
	 * objects such as sequences will be parsed lazily.
	 *
	 * @param input stream containing ASN.1 encoded data.
	 * @param limit maximum size of a DER encoded object.
	 * @param lazyEvaluate true if parsing inside constructed objects can be delayed.
	 */
	public ASN1InputStream( InputStream input, int limit, boolean lazyEvaluate ) {
		super(input);
		this.limit = limit;
		this.lazyEvaluate = lazyEvaluate;
		this.tmpBuffers = new byte[11][ ];
	}


	int getLimit() {
		return limit;
	}


	protected int readLength() throws IOException {
		return readLength(this, limit);
	}


	protected void readFully( byte[] bytes ) throws IOException {
		if ( Streams.readFully(this, bytes) != bytes.length ) {
			throw new EOFException("EOF encountered in middle of object");
		}
	}


	/**
	 * build an object given its tag and the number of bytes to construct it from.
	 */
	protected ASN1Primitive buildObject( int tag, int tagNo, int length ) throws IOException {
		boolean isConstructed = (tag & CONSTRUCTED) != 0;

		DefiniteLengthInputStream defIn = new DefiniteLengthInputStream(this, length);

		if ( (tag & APPLICATION) != 0 ) {
			return new DERApplicationSpecific(isConstructed, tagNo, defIn.toByteArray());
		}

		if ( (tag & TAGGED) != 0 ) {
			return new ASN1StreamParser(defIn).readTaggedObject(isConstructed, tagNo);
		}

		if ( isConstructed ) {
			// TODO There are other tags that may be constructed (e.g. BIT_STRING)
			switch ( tagNo ) {
				case OCTET_STRING :
					//
					// yes, people actually do this...
					//
					ASN1EncodableVector v = buildDEREncodableVector(defIn);
					ASN1OctetString[] strings = new ASN1OctetString[v.size()];

					for ( int i = 0 ; i != strings.length ; i++ ) {
						strings[i] = (ASN1OctetString) v.get(i);
					}

					return new BEROctetString(strings);
				case SEQUENCE :
					if ( lazyEvaluate ) {
						return new LazyEncodedSequence(defIn.toByteArray());
					} else {
						return DERFactory.createSequence(buildDEREncodableVector(defIn));
					}
				case SET :
					return DERFactory.createSet(buildDEREncodableVector(defIn));
				case EXTERNAL :
					return new DERExternal(buildDEREncodableVector(defIn));
				default:
					throw new IOException("unknown tag " + tagNo + " encountered");
			}
		}

		return createPrimitiveDERObject(tagNo, defIn, tmpBuffers);
	}


	ASN1EncodableVector buildEncodableVector() throws IOException {
		ASN1EncodableVector v = new ASN1EncodableVector();
		ASN1Primitive o;

		while ( (o = readObject()) != null ) {
			v.add(o);
		}

		return v;
	}


	ASN1EncodableVector buildDEREncodableVector( DefiniteLengthInputStream dIn ) throws IOException {
		return new ASN1InputStream(dIn).buildEncodableVector();
	}


	public ASN1Primitive readObject() throws IOException {
		int tag = read();
		if ( tag <= 0 ) {
			if ( tag == 0 ) {
				throw new IOException("unexpected end-of-contents marker");
			}

			return null;
		}

		//
		// calculate tag number
		//
		int tagNo = readTagNumber(this, tag);

		boolean isConstructed = (tag & CONSTRUCTED) != 0;

		//
		// calculate length
		//
		int length = readLength();

		if ( length < 0 ) // indefinite length method
		{
			if ( !isConstructed ) {
				throw new IOException("indefinite length primitive encoding encountered");
			}

			IndefiniteLengthInputStream indIn = new IndefiniteLengthInputStream(this, limit);
			ASN1StreamParser sp = new ASN1StreamParser(indIn, limit);

			if ( (tag & APPLICATION) != 0 ) {
				return new BERApplicationSpecificParser(tagNo, sp).getLoadedObject();
			}

			if ( (tag & TAGGED) != 0 ) {
				return new BERTaggedObjectParser(true, tagNo, sp).getLoadedObject();
			}

			// TODO There are other tags that may be constructed (e.g. BIT_STRING)
			switch ( tagNo ) {
				case OCTET_STRING :
					return new BEROctetStringParser(sp).getLoadedObject();
				case SEQUENCE :
					return new BERSequenceParser(sp).getLoadedObject();
				case SET :
					return new BERSetParser(sp).getLoadedObject();
				case EXTERNAL :
					return new DERExternalParser(sp).getLoadedObject();
				default:
					throw new IOException("unknown BER object encountered");
			}
		} else {
			try {
				return buildObject(tag, tagNo, length);
			} catch ( IllegalArgumentException e ) {
				throw new ASN1Exception("corrupted stream detected", e);
			}
		}
	}


	static int readTagNumber( InputStream s, int tag ) throws IOException {
		int tagNo = tag & 0x1f;

		//
		// with tagged object tag number is bottom 5 bits, or stored at the start of the content
		//
		if ( tagNo == 0x1f ) {
			tagNo = 0;

			int b = s.read();

			// X.690-0207 8.1.2.4.2
			// "c) bits 7 to 1 of the first subsequent octet shall not all be zero."
			if ( (b & 0x7f) == 0 ) // Note: -1 will pass
			{
				throw new IOException("corrupted stream - invalid high tag number found");
			}

			while ( (b >= 0) && ((b & 0x80) != 0) ) {
				tagNo |= (b & 0x7f);
				tagNo <<= 7;
				b = s.read();
			}

			if ( b < 0 ) {
				throw new EOFException("EOF found inside tag value.");
			}

			tagNo |= (b & 0x7f);
		}

		return tagNo;
	}


	static int readLength( InputStream s, int limit ) throws IOException {
		int length = s.read();
		if ( length < 0 ) {
			throw new EOFException("EOF found when length expected");
		}

		if ( length == 0x80 ) {
			return -1; // indefinite-length encoding
		}

		if ( length > 127 ) {
			int size = length & 0x7f;

			// Note: The invalid long form "0xff" (see X.690 8.1.3.5c) will be caught here
			if ( size > 4 ) {
				throw new IOException("DER length more than 4 bytes: " + size);
			}

			length = 0;
			for ( int i = 0 ; i < size ; i++ ) {
				int next = s.read();

				if ( next < 0 ) {
					throw new EOFException("EOF found reading length");
				}

				length = (length << 8) + next;
			}

			if ( length < 0 ) {
				throw new IOException("corrupted stream - negative length found");
			}

			if ( length >= limit ) // after all we must have read at least 1 byte
			{
				throw new IOException("corrupted stream - out of bounds length found");
			}
		}

		return length;
	}


	private static byte[] getBuffer( DefiniteLengthInputStream defIn, byte[][] tmpBuffers ) throws IOException {
		int len = defIn.getRemaining();
		if ( defIn.getRemaining() < tmpBuffers.length ) {
			byte[] buf = tmpBuffers[len];

			if ( buf == null ) {
				buf = tmpBuffers[len] = new byte[len];
			}

			Streams.readFully(defIn, buf);

			return buf;
		} else {
			return defIn.toByteArray();
		}
	}


	private static char[] getBMPCharBuffer( DefiniteLengthInputStream defIn ) throws IOException {
		int len = defIn.getRemaining() / 2;
		char[] buf = new char[len];
		int totalRead = 0;
		while ( totalRead < len ) {
			int ch1 = defIn.read();
			if ( ch1 < 0 ) {
				break;
			}
			int ch2 = defIn.read();
			if ( ch2 < 0 ) {
				break;
			}
			buf[totalRead++] = (char) ((ch1 << 8) | (ch2 & 0xff));
		}

		return buf;
	}


	static ASN1Primitive createPrimitiveDERObject( int tagNo, DefiniteLengthInputStream defIn, byte[][] tmpBuffers )
		throws IOException {
		switch ( tagNo ) {
			case BIT_STRING :
				return DERBitString.fromInputStream(defIn.getRemaining(), defIn);
			case BMP_STRING :
				return new DERBMPString(getBMPCharBuffer(defIn));
			case BOOLEAN :
				return ASN1Boolean.fromOctetString(getBuffer(defIn, tmpBuffers));
			case ENUMERATED :
				return ASN1Enumerated.fromOctetString(getBuffer(defIn, tmpBuffers));
			case GENERALIZED_TIME :
				return new ASN1GeneralizedTime(defIn.toByteArray());
			case GENERAL_STRING :
				return new DERGeneralString(defIn.toByteArray());
			case IA5_STRING :
				return new DERIA5String(defIn.toByteArray());
			case INTEGER :
				return new ASN1Integer(defIn.toByteArray());
			case NULL :
				return DERNull.INSTANCE; // actual content is ignored (enforce 0 length?)
			case NUMERIC_STRING :
				return new DERNumericString(defIn.toByteArray());
			case OBJECT_IDENTIFIER :
				return ASN1ObjectIdentifier.fromOctetString(getBuffer(defIn, tmpBuffers));
			case OCTET_STRING :
				return new DEROctetString(defIn.toByteArray());
			case PRINTABLE_STRING :
				return new DERPrintableString(defIn.toByteArray());
			case T61_STRING :
				return new DERT61String(defIn.toByteArray());
			case UNIVERSAL_STRING :
				return new DERUniversalString(defIn.toByteArray());
			case UTC_TIME :
				return new ASN1UTCTime(defIn.toByteArray());
			case UTF8_STRING :
				return new DERUTF8String(defIn.toByteArray());
			case VISIBLE_STRING :
				return new DERVisibleString(defIn.toByteArray());
			default:
				throw new IOException("unknown tag " + tagNo + " encountered");
		}
	}
}
